﻿using System;
using System.Collections.Generic;
using System.Text;

namespace Silver3D.Graphics
{
    /// <summary>
    /// Represents a managed SceneObjects collection.
    /// The Scene owns all of its objects, and manages the uniqueness of their IDs.
    /// </summary>
    public sealed class Scene
    {
        private object syncRoot = new object();
        private Dictionary<string, SceneObject> objects = new Dictionary<string, SceneObject>();

        /// <summary>
        /// The number of objects contained in the Scene.
        /// </summary>
        public int ObjectCount { get { return objects.Count; } }

        /// <summary>
        /// The root Node of this Scene.
        /// Create at construction time, and maintained if detached.
        /// </summary>
        public Node Root { get; private set; }

        /// <summary>
        /// Creates an empty Scene.
        /// </summary>
        public Scene()
        {
            OnRootDetached(null);
        }

        private void OnRootDetached(SceneObject obj)
        {
            if (obj != Root) return;
            Root = new Node(this, "Root");
            Root.Detached += new ObjectDetachedHandler(OnRootDetached);
        }

        internal void RegisterObject(SceneObject obj, string desiredId)
        {
            if (obj == null)
                throw new ArgumentNullException("obj");

            if (obj.Scene == this)
            {
                AssignId(obj, desiredId);
                return;
            }

            lock (syncRoot)
            {
                obj.Scene = this;
                AssignId(obj, desiredId);
                obj.Detached += new ObjectDetachedHandler(OnObjectDetached);
            }
        }

        private void OnObjectDetached(SceneObject obj)
        {
            lock (syncRoot)
            {
                if (obj == null || obj.Scene != this || obj.Id == null) return;
                objects.Remove(obj.Id);
                obj.Scene = null;
            }
        }

        /// <summary>
        /// Retrieves a SceneObject found within this Scene by providing its Id.
        /// </summary>
        /// <param name="id">The object Id.</param>
        /// <returns>The unique object, or null if not found.</returns>
        public SceneObject GetObjectById(string id)
        {
            lock (syncRoot)
            {
                if (objects.ContainsKey(id))
                {
                    return objects[id];
                }
                else
                    return null;
            }
        }

        #region Unique ID generation

        /// <summary>
        /// Assigns a new unique ID to the provided object.
        /// </summary>
        /// <param name="obj">The object to modify.</param>
        /// <param name="desiredId">The new desired ID. The given ID will attempt to match this parameter, but no guarantee is given.</param>
        public void AssignId(SceneObject obj, string desiredId)
        {
            if (obj == null)
                throw new ArgumentNullException("obj");

            if (obj.Scene != this)
                throw new Silver3DException("The object provided does not belong to this scene.");

            if (obj.Id != null && obj.Id == desiredId) return;

            lock (syncRoot)
            {
                if (obj.Id != null) objects.Remove(obj.Id);
                obj.Id = CreateUniqueId(desiredId ?? obj.GetType().Name);
                objects.Add(obj.Id, obj);
            }
        }

        private void SplitName(string name, out string alpha, out int numeric)
        {
            int lastNumeric = name.Length;
            while (lastNumeric > 0 && char.IsNumber(name[lastNumeric - 1])) --lastNumeric;
            while (lastNumeric < name.Length && name[lastNumeric] == '0') ++lastNumeric;
            if (lastNumeric == name.Length)
            {
                alpha = name;
                numeric = 2;
                return;
            }
            alpha = lastNumeric > 0 ? name.Substring(0, lastNumeric) : string.Empty;
            numeric = int.Parse(name.Substring(lastNumeric)) + 1;
        }

        private string ValidateName(string name)
        {
            if (string.IsNullOrEmpty(name)) name = "_";
            StringBuilder sb = new StringBuilder(name.Length);
            foreach (char c in name)
                if (char.IsLetterOrDigit(c)) sb.Append(c);
                else sb.Append('_');
            return sb.ToString();
        }

        private string CreateUniqueId(string desiredId)
        {
            desiredId = ValidateName(desiredId);

            string alpha;
            int numeric;
            SplitName(desiredId, out alpha, out numeric);

            while (objects.ContainsKey(desiredId))
            {
                desiredId = alpha + numeric.ToString();
                ++numeric;
            }
            return desiredId;
        }

        #endregion
    }
}
